// SPDX-License-Identifier: LGPL-2.1+

#include "nm-default.h"

#include "nmcs-provider-azure.h"

#include "nm-cloud-setup-utils.h"

/*****************************************************************************/

#define HTTP_TIMEOUT_MS 3000

#define NM_AZURE_METADATA_HEADER    "Metadata:true"
#define NM_AZURE_HOST               "169.254.169.254"
#define NM_AZURE_BASE               "http://" NM_AZURE_HOST
#define NM_AZURE_API_VERSION        "?format=text&api-version=2017-04-02"
#define NM_AZURE_METADATA_URL_BASE  /* $NM_AZURE_BASE/$NM_AZURE_API_VERSION */ "/metadata/instance/network/interface/"

#define _azure_uri_concat(...)     nmcs_utils_uri_build_concat (NM_AZURE_BASE, __VA_ARGS__, NM_AZURE_API_VERSION)
#define _azure_uri_interfaces(...) _azure_uri_concat (NM_AZURE_METADATA_URL_BASE, ##__VA_ARGS__)

/*****************************************************************************/

struct _NMCSProviderAzure {
	NMCSProvider parent;
};

struct _NMCSProviderAzureClass {
	NMCSProviderClass parent;
};

G_DEFINE_TYPE (NMCSProviderAzure, nmcs_provider_azure, NMCS_TYPE_PROVIDER);

/*****************************************************************************/

static void
_detect_get_meta_data_done_cb (GObject *source,
                               GAsyncResult *result,
                               gpointer user_data)
{
	gs_unref_object GTask *task = user_data;
	gs_free_error GError *get_error = NULL;
	gs_free_error GError *error = NULL;
	gboolean success;

	success = nm_http_client_poll_get_finish (NM_HTTP_CLIENT (source),
	                                          result,
	                                          NULL,
	                                          NULL,
	                                          &get_error);

	if (nm_utils_error_is_cancelled (get_error)) {
		g_task_return_error (task, g_steal_pointer (&get_error));
		return;
	}

	if (get_error) {
		nm_utils_error_set (&error,
		                    NM_UTILS_ERROR_UNKNOWN,
		                    "failure to get Azure metadata: %s",
		                    get_error->message);
		g_task_return_error (task, g_steal_pointer (&error));
		return;
	}

	if (!success) {
		nm_utils_error_set (&error,
		                    NM_UTILS_ERROR_UNKNOWN,
		                    "failure to detect azure metadata");
		g_task_return_error (task, g_steal_pointer (&error));
		return;
	}

	g_task_return_boolean (task, TRUE);
}

static void
detect (NMCSProvider *provider,
        GTask *task)
{
	NMHttpClient *http_client;
	gs_free char *uri = NULL;

	http_client = nmcs_provider_get_http_client (provider);

	nm_http_client_poll_get (http_client,
	                         (uri = _azure_uri_concat ("/metadata/instance")),
	                         HTTP_TIMEOUT_MS,
	                         256*1024,
	                         7000,
	                         1000,
	                         NM_MAKE_STRV (NM_AZURE_METADATA_HEADER),
	                         g_task_get_cancellable (task),
	                         NULL,
	                         NULL,
	                         _detect_get_meta_data_done_cb,
	                         task);
}

/*****************************************************************************/

typedef struct {
	NMCSProviderGetConfigTaskData *config_data;
	guint n_ifaces_pending;
	GError *error;
} AzureData;

typedef struct {
	NMCSProviderGetConfigIfaceData *iface_get_config;
	AzureData *azure_data;
	gssize iface_idx;
	guint n_ips_prefix_pending;
	char *hwaddr;
} AzureIfaceData;

static void
_azure_iface_data_free (AzureIfaceData *iface_data)
{
	g_free(iface_data->hwaddr);
	nm_g_slice_free (iface_data);
}

static void
_get_config_maybe_task_return (AzureData *azure_data,
                               GError *error_take)
{
	NMCSProviderGetConfigTaskData *config_data =  azure_data->config_data;

	if (error_take) {
		if (!azure_data->error)
			azure_data->error = error_take;
		else if (   !nm_utils_error_is_cancelled (azure_data->error)
		         && nm_utils_error_is_cancelled (error_take)) {
			nm_clear_error (&azure_data->error);
			azure_data->error = error_take;
		} else
			g_error_free (error_take);
	}

	if (azure_data->n_ifaces_pending > 0)
		return;

	if (azure_data->error) {
		if (nm_utils_error_is_cancelled (azure_data->error))
			_LOGD ("get-config: cancelled");
		else
			_LOGD ("get-config: failed: %s", azure_data->error->message);
		g_task_return_error (config_data->task, g_steal_pointer (&azure_data->error));
	} else {
		_LOGD ("get-config: success");
		g_task_return_pointer (config_data->task,
		                       g_hash_table_ref (config_data->result_dict),
		                       (GDestroyNotify) g_hash_table_unref);
	}

	nm_g_slice_free (azure_data);
	g_object_unref (config_data->task);
}

static void
_get_config_fetch_done_cb (NMHttpClient *http_client,
                           GAsyncResult *result,
                           gpointer user_data,
                           gboolean is_ipv4)
{
	NMCSProviderGetConfigIfaceData *iface_get_config;
	gs_unref_bytes GBytes *response = NULL;
	AzureIfaceData *iface_data = user_data;
	gs_free_error GError *error = NULL;
	const char *fip_str = NULL;
	AzureData *azure_data;

	azure_data = iface_data->azure_data;

	nm_http_client_poll_get_finish (http_client,
	                                result,
	                                NULL,
	                                &response,
	                                &error);

	if (error)
		goto done;

	if(!error){
		in_addr_t tmp_addr;
		int tmp_prefix;

		fip_str = g_bytes_get_data (response, NULL);
		iface_data->iface_get_config = g_hash_table_lookup (azure_data->config_data->result_dict,
		                                                    iface_data->hwaddr);
		iface_get_config = iface_data->iface_get_config;
		iface_get_config->iface_idx = iface_data->iface_idx;

		if (is_ipv4) {
			if (!nm_utils_parse_inaddr_bin (AF_INET,
			                               fip_str,
			                               NULL,
			                               &tmp_addr)) {
				error = nm_utils_error_new (NM_UTILS_ERROR_UNKNOWN,
				                            "ip is not a valid private ip address");
				goto done;
			}
			_LOGD ("interface[%"G_GSSIZE_FORMAT"]: adding private ip %s",
			       iface_data->iface_idx,
			       fip_str);
			iface_get_config->ipv4s_arr[iface_get_config->ipv4s_len] = tmp_addr;
			iface_get_config->has_ipv4s = TRUE;
			iface_get_config->ipv4s_len++;
		} else {
			tmp_prefix = (_nm_utils_ascii_str_to_int64 (fip_str, 10, 0, 32, -1));

			if (tmp_prefix == -1) {
				_LOGD ("interface[%"G_GSSIZE_FORMAT"]: invalid prefix %d",
				       iface_data->iface_idx,
				       tmp_prefix);
				goto done;
			}
			_LOGD ("interface[%"G_GSSIZE_FORMAT"]: adding prefix %d",
			       iface_data->iface_idx,
			       tmp_prefix);
			iface_get_config->cidr_prefix = tmp_prefix;
			iface_get_config->has_cidr = TRUE;
		}
	}

done:
	--iface_data->n_ips_prefix_pending;
	if (iface_data->n_ips_prefix_pending == 0) {
		_azure_iface_data_free (iface_data);
		--azure_data->n_ifaces_pending;
		_get_config_maybe_task_return (azure_data, g_steal_pointer (&error));
	}
}

static void
_get_config_fetch_done_cb_private_ipv4s (GObject *source,
                                         GAsyncResult *result,
                                         gpointer user_data)
{
	_get_config_fetch_done_cb (NM_HTTP_CLIENT (source), result, user_data, TRUE);
}

static void
_get_config_fetch_done_cb_subnet_cidr_prefix (GObject *source,
                                               GAsyncResult *result,
                                               gpointer user_data)
{
	_get_config_fetch_done_cb (NM_HTTP_CLIENT (source), result, user_data, FALSE);
}

static void
_get_config_ips_prefix_list_cb (GObject *source,
                                GAsyncResult *result,
                                gpointer user_data)
{
	gs_unref_bytes GBytes *response = NULL;
	AzureIfaceData *iface_data = user_data;
	gs_free_error GError *error = NULL;
	const char *response_str = NULL;
	gsize response_len;
	AzureData *azure_data;
	const char *line;
	gsize line_len;

	azure_data = iface_data->azure_data;

	nm_http_client_poll_get_finish (NM_HTTP_CLIENT (source),
	                                result,
	                                NULL,
	                                &response,
	                                &error);
	if (error)
		goto done;

	response_str = g_bytes_get_data (response, &response_len);
	/* NMHttpClient guarantees that there is a trailing NUL after the data. */
	nm_assert (response_str[response_len] == 0);

	nm_assert (!iface_data->iface_get_config->has_ipv4s);
	nm_assert (!iface_data->iface_get_config->ipv4s_arr);
	nm_assert (!iface_data->iface_get_config->has_cidr);

	while (nm_utils_parse_next_line (&response_str,
	                                 &response_len,
	                                 &line,
	                                 &line_len)) {
		gint64 ips_prefix_idx;

		if (line_len == 0)
			continue;
		/* Truncate the string. It's safe to do, because we own @response_data an it has an
		 * extra NULL character after the buffer. */
		((char *) line)[line_len] = '\0';

		if (line[line_len - 1] == '/')
			((char *) line)[--line_len] = '\0';

		ips_prefix_idx = _nm_utils_ascii_str_to_int64 (line, 10, 0, G_MAXINT64, -1);

		if (ips_prefix_idx < 0)
			continue;

		{
			gs_free const char *uri = NULL;
			char buf[100];

			iface_data->n_ips_prefix_pending++;

			nm_http_client_poll_get (NM_HTTP_CLIENT (source),
			                         (uri = _azure_uri_interfaces (nm_sprintf_buf (buf,"%"G_GSSIZE_FORMAT"/ipv4/ipAddress/%"G_GINT64_FORMAT"/privateIpAddress",
			                                                                       iface_data->iface_idx,
			                                                                       ips_prefix_idx))),
			                         HTTP_TIMEOUT_MS,
			                         512*1024,
			                         10000,
			                         1000,
			                         NM_MAKE_STRV (NM_AZURE_METADATA_HEADER),
			                         g_task_get_cancellable (azure_data->config_data->task),
			                         NULL,
			                         NULL,
			                         _get_config_fetch_done_cb_private_ipv4s,
			                         iface_data);
		}
	}

	iface_data->iface_get_config->ipv4s_len = 0;
	iface_data->iface_get_config->ipv4s_arr =
		g_new (in_addr_t , iface_data->n_ips_prefix_pending);

	{
		gs_free const char *uri = NULL;
		char buf[30];

		iface_data->n_ips_prefix_pending++;
		nm_http_client_poll_get (NM_HTTP_CLIENT (source),
		                         (uri = _azure_uri_interfaces (nm_sprintf_buf (buf, "%"G_GSSIZE_FORMAT, iface_data->iface_idx),
		                                                       "/ipv4/subnet/0/prefix/")),
		                         HTTP_TIMEOUT_MS,
		                         512*1024,
		                         10000,
		                         1000,
		                         NM_MAKE_STRV (NM_AZURE_METADATA_HEADER),
		                         g_task_get_cancellable (azure_data->config_data->task),
		                         NULL,
		                         NULL,
		                         _get_config_fetch_done_cb_subnet_cidr_prefix,
		                         iface_data);
	}
	return;

done:
	_azure_iface_data_free (iface_data);
	--azure_data->n_ifaces_pending;
	_get_config_maybe_task_return (azure_data, g_steal_pointer (&error));
}

static void
_get_config_iface_cb (GObject *source,
                      GAsyncResult *result,
                      gpointer user_data)
{
	gs_unref_bytes GBytes *response = NULL;
	AzureIfaceData *iface_data = user_data;
	gs_free_error GError *error = NULL;
	gs_free const char *uri = NULL;
	char buf[100];
	AzureData *azure_data;

	azure_data = iface_data->azure_data;

	nm_http_client_poll_get_finish (NM_HTTP_CLIENT (source),
	                                result,
	                                NULL,
	                                &response,
	                                &error);

	if (error)
		goto done;

	iface_data->hwaddr = nmcs_utils_hwaddr_normalize (g_bytes_get_data (response, NULL), -1);

	if (!iface_data->hwaddr) {
		goto done;
	}

	iface_data->iface_get_config = g_hash_table_lookup (azure_data->config_data->result_dict,
	                                                    iface_data->hwaddr);

	if (!iface_data->iface_get_config) {
		if (!iface_data->azure_data->config_data->any) {
			_LOGD ("interface[%"G_GSSIZE_FORMAT"]: ignore hwaddr %s",
			       iface_data->iface_idx,
			       iface_data->hwaddr);
			goto done;
		}
		iface_data->iface_get_config = nmcs_provider_get_config_iface_data_new (FALSE);
		g_hash_table_insert (azure_data->config_data->result_dict,
		                     g_strdup (iface_data->hwaddr),
		                     iface_data->iface_get_config);
	}

	_LOGD ("interface[%"G_GSSIZE_FORMAT"]: found a matching device with hwaddr %s",
	       iface_data->iface_idx,
	       iface_data->hwaddr);

	nm_sprintf_buf (buf, "%"G_GSSIZE_FORMAT"/ipv4/ipAddress/", iface_data->iface_idx);

	nm_http_client_poll_get (NM_HTTP_CLIENT (source),
	                         (uri = _azure_uri_interfaces (buf)),
	                         HTTP_TIMEOUT_MS,
	                         512*1024,
	                         10000,
	                         1000,
	                         NM_MAKE_STRV (NM_AZURE_METADATA_HEADER),
	                         g_task_get_cancellable (azure_data->config_data->task),
	                         NULL,
	                         NULL,
	                         _get_config_ips_prefix_list_cb,
	                         iface_data);
	return;

done:
	nm_g_slice_free (iface_data);
	--azure_data->n_ifaces_pending;
	_get_config_maybe_task_return (azure_data, g_steal_pointer (&error));
}

static void
_get_net_ifaces_list_cb (GObject *source,
                         GAsyncResult *result,
                         gpointer user_data)
{
	gs_unref_ptrarray GPtrArray *ifaces_arr = NULL;
	gs_unref_bytes GBytes *response = NULL;
	gs_free_error GError *error = NULL;
	AzureData *azure_data = user_data;
	const char *response_str;
	gsize response_len;
	const char *line;
	gsize line_len;
	guint i;

	nm_http_client_poll_get_finish (NM_HTTP_CLIENT (source),
	                                result,
	                                NULL,
	                                &response,
	                                &error);

	if (error) {
		_get_config_maybe_task_return (azure_data, g_steal_pointer (&error));
		return;
	}

	response_str = g_bytes_get_data (response, &response_len);
	/* NMHttpClient guarantees that there is a trailing NUL after the data. */
	nm_assert (response_str[response_len] == 0);

	ifaces_arr = g_ptr_array_new ();

	while (nm_utils_parse_next_line (&response_str,
	                                 &response_len,
	                                 &line,
	                                 &line_len)) {
		AzureIfaceData *iface_data;
		gssize iface_idx;

		if (line_len == 0)
			continue;

		/* Truncate the string. It's safe to do, because we own @response_data an it has an
		 * extra NULL character after the buffer. */
		((char *) line)[line_len] = '\0';

		if (line[line_len - 1] == '/' && line_len != 0)
			((char *) line)[--line_len] = '\0';

		iface_idx = _nm_utils_ascii_str_to_int64 (line, 10, 0, G_MAXSSIZE, -1);
		if (iface_idx < 0)
			continue;

		iface_data = g_slice_new (AzureIfaceData);
		*iface_data = (AzureIfaceData) {
			.iface_get_config = NULL,
			.azure_data = azure_data,
			.iface_idx = iface_idx,
			.n_ips_prefix_pending = 0,
			.hwaddr = NULL,
		};
		g_ptr_array_add (ifaces_arr, iface_data);
	}

	_LOGD ("found azure interfaces: %u", ifaces_arr->len);

	if (ifaces_arr->len == 0) {
		error = nm_utils_error_new (NM_UTILS_ERROR_UNKNOWN,
		                            "no Azure interfaces found");
		_get_config_maybe_task_return (azure_data, g_steal_pointer (&error));
		return;
	}

	for (i = 0; i < ifaces_arr->len; ++i) {
		AzureIfaceData *data = ifaces_arr->pdata[i];
		gs_free const char *uri = NULL;
		char buf[100];

		_LOGD ("azure interface[%"G_GSSIZE_FORMAT"]: retrieving configuration",
		       data->iface_idx);

		nm_sprintf_buf (buf, "%"G_GSSIZE_FORMAT"/macAddress", data->iface_idx);

		azure_data->n_ifaces_pending++;
		nm_http_client_poll_get (NM_HTTP_CLIENT (source),
		                         (uri = _azure_uri_interfaces (buf)),
		                         HTTP_TIMEOUT_MS,
		                         512*1024,
		                         10000,
		                         1000,
		                         NM_MAKE_STRV (NM_AZURE_METADATA_HEADER),
		                         g_task_get_cancellable (azure_data->config_data->task),
		                         NULL,
		                         NULL,
		                         _get_config_iface_cb,
		                         data);
	}
}

static void
get_config (NMCSProvider *provider,
            NMCSProviderGetConfigTaskData *get_config_data)
{
	gs_free const char *uri = NULL;
	AzureData *azure_data;

	azure_data = g_slice_new (AzureData);
	*azure_data = (AzureData) {
		.config_data = get_config_data,
		.n_ifaces_pending = 0,
	};

	nm_http_client_poll_get (nmcs_provider_get_http_client (provider),
	                         (uri = _azure_uri_interfaces ()),
	                         HTTP_TIMEOUT_MS,
	                         256 * 1024,
	                         15000,
	                         1000,
	                         NM_MAKE_STRV (NM_AZURE_METADATA_HEADER),
	                         g_task_get_cancellable (get_config_data->task),
	                         NULL,
	                         NULL,
	                         _get_net_ifaces_list_cb,
	                         azure_data);
}

/*****************************************************************************/

static void
nmcs_provider_azure_init (NMCSProviderAzure *self)
{
}

static void
nmcs_provider_azure_class_init (NMCSProviderAzureClass *klass)
{
	NMCSProviderClass *provider_class = NMCS_PROVIDER_CLASS (klass);

	provider_class->_name                 = "azure";
	provider_class->_env_provider_enabled = NMCS_ENV_VARIABLE ("NM_CLOUD_SETUP_AZURE");
	provider_class->detect                = detect;
	provider_class->get_config            = get_config;
}
